1 Introducing SwiftUI Animations¶
Small touches can help your app stand out from the competition in the crowded App Store. Animations provide one of these small delightful details.
Used correctly, animations show an attention to detail that your users will appreciate and a unique style they’ll remember. Using animations purposefully provides your users subtle and practical feedback as your app’s state changes.
Up until the release of SwiftUI, creating animations was quite a tedious task, even for the simplest of animations. Luckily, SwiftUI is often clever enough to automatically animate your state changes, or provide you with more granular control when the default animations don’t cut it.
First, you’ll explore the basic native animations included in SwiftUI resulting from state changes, the transformation of a value that a view depends on. You’ll then explore view transitions, a type of animation that SwiftUI applies to views when inserted or removed from the screen. These animations provide a base of knowledge you’ll use throughout this book.
Creating Animations¶
To begin, download and extract the materials for this chapter. Open the starter folder, which contains the starter project for this chapter. Then, open AnimationCompare.xcodeproj to start working on this chapter.
Run the project by selecting Product ▸ Run or press Cmd-R. When the project starts in the simulator, you’ll see two tabs:
The first tab contains the user interface for an app that helps a developer explore different types of animations and manipulate various animation parameters to see their effects. The user can add multiple animations and run them in tandem.
The second tab contains a red square you can show or hide using a button. You’ll use this tab to explore view transitions later in this chapter.
Exploring the Starter App¶
Inside the Models folder, open AnimationData.swift. You’ll find the AnimationData
struct, which holds the properties used by the different types of built-in animations.
Open AnimationCompareView.swift and look for the Add Animation button. When the user taps it, the app creates a new struct with a set of default values and adds it to the animations
array. The user can change these values, but none of the animations work yet. You’ll fix that now.
First, look for the location
state property:
@State var location = 0.0
As the state changes, you’ll use this property to animate views in your app. First, you need to provide a way for the user to change this state. Immediately inside the VStack
, add the following code:
// 1
Button("Animate!") {
// 2
location = location == 0 ? 1 : 0
}
.font(.title)
.disabled(animations.isEmpty)
The code above:
- Creates a button that changes the state property
location
to animate views when tapped. - Toggles the value of
location
between 0.0 and 1.0. You’ll use this later to animate the views on screen.
Notice that you disable the button when the animations
array is empty. This prevents users from tapping the button before creating any animations.
Adding Your First Animation¶
Open AnimationView.swift. Near the top of the view, look for this line:
@Binding var location: Double
This property contains the location
passed in from the parent view. When the value changes in AnimationCompareView
, it also changes inside this view, since it’s a Binding
. Inside each AnimationView
, SwiftUI will notice the state change and trigger two animations that you specify.
Currently, AnimationView
contains a Text
view wrapped inside a GeometryReader
. Replace this Text
view with:
HStack {
// 1
Image(systemName: "gear.circle")
.rotationEffect(.degrees(360 * location))
Image(systemName: "star.fill")
// 2
.offset(x: proxy.size.width * location * 0.8)
}
.font(.title)
// 3
.animation(
// 4
.linear(duration: animation.length),
// 5
value: location
)
Here’s what each part of the new view does:
- You place two images in an
HStack
. You apply a rotation effect to the first image that multiplies thelocation
property by 360 degrees. Sincelocation
will vary between zero and one, the result will toggle between zero and 360 degrees. The key is that a change inlocation
changes the view’s state. - The second image has an offset applied that multiples the width of the view, taken from the
GeometryProxy
, by thelocation
property and multiples that by0.8
. As a result, whenlocation
is zero, the offset is zero, and whenlocation
is one, the offset is 80% of the width of the view. Since SwiftUI applies the offset to the view’s leading edge, multiplying by 0.8 keeps the view from floating off the screen. - There are several ways to tell SwiftUI you want to animate a state change. Here you use
animation(_:value:)
on theHStack
. This method creates the most straightforward SwiftUI animation, an implicit animation. You apply it to theHStack
so that both views included within have the animation applied to them. Sounds simple? That’s the beauty of animations in SwiftUI! - The first parameter to
animation(_:value:)
defines the type of animation, which is a linear animation in this case. You then pass theduration
parameter, telling SwiftUI the animation should take the amount of time specified inanimation.length
to complete. Most animations should last between 0.25 and 1.0 seconds as these values allow the user time to notice the animation without feeling too long and intrusive. - When you apply an implicit animation, you specify the value whose change will trigger the animation. Explicitly setting the state change lets you use different animations with different state changes.
Run the app and add an animation. Next, tap Animate!. The gear icon makes one revolution, and the star slides to the right side of the view.
If you tap Animate! again, you’ll see the gear spin in the opposite direction while the star slides back to the left. Think for a moment about why the opposite movement takes place. Here’s a hint: remember what the Animate! button does.
Since the Animate! button returns the property to its original value of zero, the animation reverses. The rotationEffect(_:anchor:)
method interprets greater values as clockwise rotation. Therefore, the initial change from zero to one turns into a degree change from zero to 360. This change animates as a set of increasing clockwise rotations. The change back to zero causes counterclockwise animation as the value decreases.
Linear animations work best for views that pass through but do not start or end within the scene. In the real world, a car passing by a window would look routine while moving at a constant speed, but a vehicle instantly achieving full speed from a stop would seem odd. Our minds expect something that starts or stops within our view to accelerate or decelerate.
In the next section, you’ll explore eased animations, another type of animation that matches this behavior.
Creating Eased Animations¶
Instead of linear movement, eased animations provide acceleration or deceleration at one or both endpoints. The types of eased animations differ by where the change in speed applies.
The most common is the ease out animation. It starts faster than a linear animation before decelerating toward the end. Ease out animations are often the best choice in a user interface since that fast initial motion gives the feeling your app is quickly responding to the user. Here’s the graph of the movement against time:
An ease in animation reverses these steps. It starts more slowly than a linear animation before accelerating. If you were to graph the movement against time, it would look like this:
The next eased animation combines the previous two. Ease in-out animations accelerate, as in the ease in animation, before decelerating, as in the ease out animation. For ease in and ease in-out animations, you usually want to keep the duration less than 0.5 seconds so it feels more responsive to your user.
The movement graphed against time looks like a combination of the other two graphs:
Applying Eased Animations¶
The app already lets the user select eased animations, so next, you’ll add support for those. Open AnimationView.swift and add the following new computed property after the location
property:
var currentAnimation: Animation {
switch animation.type {
case .easeIn:
return Animation.easeIn(duration: animation.length)
case .easeOut:
return Animation.easeOut(duration: animation.length)
case .easeInOut:
return Animation.easeInOut(duration: animation.length)
default:
return Animation.linear(duration: animation.length)
}
}
This computed property converts the AnimationType
enum of animation
to the matching SwiftUI animation using a duration from the length
property.
Next, change the animation(_:value:)
modifier to:
.animation(
currentAnimation,
value: location
)
This code sets the animation using the new computed property you just added, so it has the animation specified in the animation
struct. Any animation you haven’t implemented will fall back to a linear animation.
Run the app and create two animations. Tap the second and change the type to Ease In-Out.
Tap Back and then Animate! to see the difference between the two animations.
You can slow down animations inside the Simulator using the Debug ▸ Slow Animationstoggle to better view the sometimes subtle differences between animations.
Notice the ease in out animation moves slower at first before passing the linear animation and then slowing down at the end. Since you specified the same duration
for both animations, they take the same time to complete.
Add two more animations and set one to Ease In and the other to Ease Out. Change the length of one animation and rerun it to see how they compare. Notice the shape of the animation doesn’t change. Only the time it takes the animation to complete changes.
While the linear and eased animations let you set the animation’s duration, SwiftUI also provides general modifiers you can apply to any animation.
In the next section, you’ll learn two common modifiers that help you customize the duration of your animation.
Modifying Animations¶
By default, an animation starts immediately when the state changes, but since your app lets the user specify a delay for each animation, you’ll add support for it. Open AnimationView.swift and replace the current animation(_:value:)
modifier with:
.animation(
currentAnimation
.delay(animation.delay),
value: location
)
You add the delay(_:)
modifier to the animation and specify the delay in seconds. Run the app and add two animations. Edit the second animation and set the delay to 0.5 seconds. Tap Back and tap the Animate! button to see the effect.
While the first animation begins when you tap the button, the second animation doesn’t start until 0.5 seconds later. A delay doesn’t affect the duration or movement of the animation. However, it can provide a sense of flow or order between multiple animations tied to a single state change.
Changing Animation Speed¶
Another useful modifier lets you change the animation’s speed independent of type and parameters. You add the speed(_:)
modifier and pass a ratio of the base speed to the desired speed.
A value lower than one will result in a slower animation, while a value greater than one will speed up the animation.
You’ll use this to implement a slowdown effect that will make it easier for the user to notice the differences between animations, similar to the Simulator menu option.
Open AnimationCompareView.swift. Add the following new property after the existing ones:
@State var slowMotion = false
You’ll use this boolean property to indicate when the user wants to slow the animations. Next, add the following toggle control to the view as the first item inside the List
, before the ForEach
:
Toggle("Slow Animations (¼ speed)", isOn: $slowMotion)
Now the user can use this toggle to specify when they want to slow down the animations. Next, open AnimationView.swift and add the following property.
var slowMotion = false
The parent view can now indicate when to slow down the animations on this view, defaulting to false
.
Next, replace the existing animation modifier with:
.animation(
currentAnimation
.delay(animation.delay)
.speed(slowMotion ? 0.25 : 1.0),
value: location
)
Recall that values lower than one passed to the speed(_:)
modifier cause the animation to slow down. Passing 0.25 will cause the animation to take four times as long (1 / 0.25 = 4
) as it otherwise would have.
Go back to AnimationCompareView.swift. To pass the new property to the view, add the new slowMotion
parameter to your AnimationView
:
AnimationView(
animation: animation,
location: $location,
slowMotion: slowMotion
)
Run the app, add an animation and tap the Animate! button. You’ll see the familiar one-second linear animation. Now toggle on Slow Animations and tap Animate! again.
Your animation now takes four seconds, or four times longer, to complete. To verify all animations run slower, add another animation and change its length to 0.5 seconds.
When you animate them, you’ll see the second animation takes two seconds (4 x 0.5 seconds) or half as long as the first animation.
Now that you’ve seen some of the modifiers you can apply to animations, you’ll look at the last type of animation: spring animation.
Springing Into Animations¶
Spring animations are popular because they seem more natural. They usually end with a slight overshoot and add some “bounce” at their end. The animation values come from the model of a spring attached to a weight.
Imagine a weight attached to one end of a spring. Attach the other end to a fixed point and let the spring drop vertically with the weight at the bottom. The weight will bounce several times before coming to a full stop.
The slowdown and stop come from friction acting on the system. The reduction creates a damped system. Graphing the motion over time produces a result like this:
There are two types of spring animations. The interpolatingSpring(mass:stiffness:damping:initialVelocity:)
animation uses this damped spring model to produce values. This animation type preserves velocity across overlapping animations by adding the effects of each animation together.
To experiment with this, open AnimationView.swift and add the following new case to the currentAnimation
computed property, before the default
case:
case .interpolatingSpring:
return Animation.interpolatingSpring(
// 1
mass: animation.mass,
// 2
stiffness: animation.stiffness,
// 3
damping: animation.damping,
// 4
initialVelocity: animation.initialVelocity
)
Each of the parameters maps to an element of the physical model. Here’s what they do:
- mass reflects the mass of the weight.
- stiffness defines how resistant the spring is to being stretched or compressed.
- damping maps to gravity and friction that slows down and stops the motion.
- initalVelocity reflects the weight’s velocity when the animation starts.
Notice that these parameters aren’t correlated with the linear and eased animations you used earlier. You also don’t have a direct way to set the animation length as you did before.
Run the app and add two animations.
Change the type for the second one to an Interpolating Spring and keep the default values, which include the default mass
and initialVelocity
if you don’t specify them to the method.
Go back to the main screen and tap Animate!, and you’ll see a much different animation. The star and gear move past the end point before bouncing slightly backward. The movement will repeat with the motion decreasing until it stops.
Even with the extra movement, the spring completes faster than the one-second linear animation.
Note
Before moving on, try to experiment with the different animation parameters and get a grasp for how each of them effects the animation.
Increasing the mass causes the animation to last longer and bounce further on each side of the end point. A smaller mass stops faster and moves less past the end points on each bounce.
Increasing the stiffness causes each bounce to move further past the end points but with a smaller effect on the animation’s length.
Increasing the damping slows the animation faster. If you set an initial velocity, it changes the initial movement of the animation.
Another Way to Spring¶
SwiftUI provides a second spring animation method you can apply using the spring(response:dampingFraction:blendDuration:)
method. The underlying model doesn’t change, but this method abstracts the four different physics-based arguments with two simpler arguments.
Open AnimationView.swift and add the following case before the default
case:
case .spring:
return Animation.spring(
response: animation.response,
dampingFraction: animation.dampingFraction
)
The spring
’s response
and dampingFraction
internally map to the appropriate physics-based values of interpolatingSpring
.
The response
parameter acts similarly to the mass in the physics-based model. It determines how resistant the animation is to changing speed. A larger value will result in an animation slower to speed up or slow down.
The dampingFraction
parameter controls how quickly the animation slows down. A value greater than or equal to one will cause the animation to settle without the bounce that most associate with spring animations.
A value between zero and one will create an animation that shoots past the final position and bounces a few times, similarly to the previous section.
A value near one will slow faster than a value near zero. A value of zero won’t settle and will oscillate forever, or at least until your user gets frustrated and closes your app.
Note that you aren’t using the blendDuration
parameter of spring(response:dampingFraction:blendDuration:)
as that only applies when combining multiple animations, which is more advanced than you’ll examine in this chapter.
Run the app and add two animations. Change the type of the first to Interpolating Springand the type of the second to Spring. Tap Animate!, and you’ll notice that the animations are similar despite the different parameters.
Change the first animation to Spring and experiment by changing the values to see the effect of mass
and stiffness
on the animation. Slowing the animation down will help with the often subtle differences between spring animations.
You now understand the basics of SwiftUI animations and can use the app to explore and fine-tune animations in your apps.
Next, you’ll look at one final category of animations: view transitions.
Using View Transitions¶
View transitions are a subset of animations that animate how views appear or vanish. You’ll often use them when a view only appears when your app is in specific states. Open TransitionCompareView.swift. You’ll see a simple view consisting of a button that toggles the boolean showSquare
property. When showSquare
is true
, it shows a red square with rounded corners.
Run the app, go to the Transitions tab and tap the button a few times to see it in action.
Note that there’s no animation when the square appears and vanishes. That’s because transitions only occur when you apply an animation to the state change. Earlier in the chapter, you used implicit animations where the animation(_:value:)
modifier implied SwiftUI should animate the view. Now, you will explicitly tell SwiftUI to create an animation when showSquare
changes. To do so, go to the action for the button and change it to:
withAnimation {
showSquare.toggle()
}
With the animation applied, you can apply a transition using the transition(_:)
modifier on a view. Look for the conditional statement to show the rectangle and add the following modifier after foregroundColor()
:
.transition(.scale)
Run the app and repeat the steps. You’ll see the square shrink to a single point.
You can also use this explicit withAnimation(_:_:)
function on the animations you used earlier in this chapter. However, it can only specify a single animation and will apply that animation to all changes resulting from the code within the function.
The scale transition you applied here makes the view appear to originate or vanish by scaling to a provided ratio of the view’s size. By default, the view scales down to zero at a point in its center. You can change either of these values.
Change the transition for the rounded square to:
.transition(.scale(scale: 2.0, anchor: .topLeading))
Run the app. When you hide the view, the square will expand to twice its original size, with the scaling centered around the top leading corner of the view before vanishing.
Note
When running in the simulator, the vanishing of the scale animation might end abruptly due to a bug in SwiftUI. If this happens to you, try running the code on a device, or a different Simulator.
Additional Transitions¶
You can specify the default fade transition using the opacity
transition. Change the transition to read:
.transition(.opacity)
Run the app. You’ll see the view now vanishes and appears with a fade-in/out animation.
Another transition is offset(x:y:)
, which lets you specify that the view should offset from its current position. Change the transition to read:
.transition(.offset(x: 10, y: 40))
Run the app. The view slides slightly to the right and down before being removed. When it returns, it appears at the same position it vanished from before returning to the original location.
You can also specify the view should move towards a specified edge using the move(edge:)
transition. Change the current transition to:
.transition(.move(edge: .trailing))
Run the app. Now the view slides off toward the trailing edge when you tap the button to hide it. When you show the square, the view will appear from the same edge it moved toward before vanishing.
Having a view transition by appearing on the leading edge and vanishing toward the trailing edge is common enough that SwiftUI includes a predefined specifier: the slide
transition. Change the transition to:
.transition(.slide)
Run the app, and you’ll see the view acts similar to the move(edge:)
transition it replaced, except the view now appears from the leading edge and vanishes toward the trailing edge. This transition provides different animations for inserting and removing the view.
Head over to the next section to learn how you too can create these custom asynchronous transitions!
Using Asynchronous Transitions¶
You can specify different transitions when the view appears and vanishes using the asymmetric(insertion:removal:)
method on AnyTransition
.
Add the following code after the showSquare
state property:
// 1
var squareTransition: AnyTransition {
// 2
let insertTransition = AnyTransition.move(edge: .leading)
let removeTransition = AnyTransition.scale
// 3
return AnyTransition.asymmetric(
insertion: insertTransition,
removal: removeTransition
)
}
Here’s how your new transition works:
- You specify the transition as a computed property on the view. Doing so helps keep the view code less cluttered and makes it easier to change in the future.
- Next, you create two transitions. The first is a
move(edge:)
transition and the second is ascale
transition. - You use the
.asymmetric(insertion:removal:)
transition and specify the insertion and removal transition for your view.
Finally, change the transition to read:
.transition(squareTransition)
This method tells SwiftUI to apply the transition from the squareTransition
property to the view.
Run the app. When you tap the button, you’ll see the view does as you’d expect. The view appears from the leading edge and scales down when removed.
You’ve now explored the basics of animations and view transitions in SwiftUI. In the remainder of this book, you’ll delve deeper into more complex animations.
Challenge¶
Modify the transitions view for this chapter’s app to let the user specify a single transition or separate insert and removal transitions. For each type of transition, let the user select the additional values supported by the transition. Apply these transitions to the square when the user taps the button.
To help you get started, you’ll find data structures that can hold the properties for transitions in TransitionData.swift. You’ll also find a view letting the user specify these properties in TransitionTypeView.swift. Check the challenge project in the materials for this chapter for a possible solution.
Key Points¶
- SwiftUI animations are driven by state changes. The change of a value that affects a view.
- View transitions are animations applied to views when SwiftUI inserts or removes them.
- Linear animations represent a constant-paced animation between two values.
- Eased animations apply acceleration, deceleration or both to the animation.
- Spring animations use a physics-based model of a spring.
- You can delay or change the speed of animations.
- Most animations should last between 0.25 and 1.0 seconds in length. Shorter animations often aren’t noticeable, while longer animations risk annoying your user who just wants to get something done.
- View transitions can animate by opacity, scale or movement. You can use different transitions for the insertion and removal of views.
Where to Go From Here?¶
- Chapter 19: Animations & View Transitions in SwiftUI by Tutorials contains an examination of the basics of animation within an app, including some you won’t see until later in this book.
- The WWDC 2018 session, Designing Fluid Interfaces, also details gestures and motion in apps.
- For more on how to use animations and transitions in your apps, see the Human Interface Guidelines.